package ca.swank.hq;

import ca.swank.hardquor.*;

import java.net.*;
import java.io.*;
import java.util.TreeMap;
import java.util.HashMap;
import java.util.StringTokenizer;
import java.util.ArrayList;

public class hqGameServer
{

  private static final int LOG_IN = 0;
  private static final int LOG_OUT = 1;
  private static final int LOG_CRITICAL = 2;
  private static final int LOG_WARNING = 3;
  private static final String[] LOG_DIRECTIONS;

  static
  { LOG_DIRECTIONS = new String[4];
	LOG_DIRECTIONS[LOG_IN] = "<";
    LOG_DIRECTIONS[LOG_OUT] = ">";
    LOG_DIRECTIONS[LOG_CRITICAL] = " ! ";
    LOG_DIRECTIONS[LOG_WARNING] = "*";
  }

  static int gamesCreated;

  private TreeMap registeredUsers;
  private HashMap activeGames;

  public hqGameServer()
  { registeredUsers = new TreeMap();
    activeGames = new HashMap();
  }

  public void start()
  {
    gamesCreated = 0;

	ServerSocket server = null;

    try
    { server = new ServerSocket(867);
    } catch(IOException e)
    { log("S",LOG_CRITICAL,"server socket create exception");
    }

    while(true)
    {
      try
      { Socket connection = server.accept();
        hardquorUserSession unregistered = new hardquorUserSession(connection);

	  } catch(IOException e)
	  { log("S",LOG_CRITICAL,"connection make exception");
      }
    }

  }


  public static void main(String[] args)
  {
    hqGameServer server = new hqGameServer();
    server.start();
  }

  public String isValidUsername(String username)
  { if(username.length() < 2) return "short";
    else if(username.length() > 10) return "long";
	else if(registeredUsers.containsKey(username)) return "taken";
	else
	{
      String acceptableChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-0123456789";
      for(int i = 0; i < username.length(); i++)
      { if(acceptableChars.indexOf(username.charAt(i)) == -1) return "illegalchar";
      }
    }
    return null;
  }

  public void registerUserSession(String username, hardquorUserSession userSession)
  { registeredUsers.put(username, userSession);

    hardquorUserSession[] sessions = getSessions(0);
    for(int i = 0; i < sessions.length; i++)
    { sessions[i].notifyUserNew(username);
    }
  }

  public void deregisterUserSession(hardquorUserSession user)
  {
    registeredUsers.remove(user.getUsername());

    hardquorUserSession[] sessions = getSessions(0);
    for(int i = 0; i < sessions.length; i++)
    { sessions[i].notifyUserGone(user.getUsername());
    }
  }

  public hardquorUserSession[] getSessions(int area)
  { if(area == 0) return (hardquorUserSession[]) registeredUsers.values().toArray(new hardquorUserSession[] { });
    else
    { hardquorGame game = (hardquorGame)activeGames.get(new Integer(area));
      if(game != null) return game.getSessions();
      else return new hardquorUserSession[] { };
    }
  }

  public hardquorGame getGame(int gameNumber)
  { return (hardquorGame)activeGames.get(new Integer(gameNumber));
  }

  class hardquorGame
  {

    private int gameNumber;
    private hqBoard gameBoard;
    private hardquorUserSession[] gamePlayers;

    public hardquorGame(hardquorUserSession requestMaker, int requestMakerPlayerNumber, String requestTargetUsername)
    {
	  hardquorUserSession requestTarget = (hardquorUserSession)registeredUsers.get(requestTargetUsername);
	  gamePlayers = new hardquorUserSession[2];

	  int requestTargetPlayerNumber = -1;

      if(requestTarget != null)
      {
		if(requestMakerPlayerNumber == 0)
        { requestTargetPlayerNumber = 1;
        } else
        { requestTargetPlayerNumber = 0;
	    }

	    gamePlayers[requestTargetPlayerNumber] = requestTarget;
        gamePlayers[requestMakerPlayerNumber] = requestMaker;

	    gameNumber = ++gamesCreated;

	    requestTarget.notifyGameRequest(gameNumber, requestTargetPlayerNumber, requestMaker.getUsername());

	    activeGames.put(new Integer(gameNumber), this);
	  }

	}

	public void requestAccepted()
	{ if(gameBoard == null)
	  { gameBoard = new hqBoard();

	    gamePlayers[0].notifyGameStart(gameNumber, 0, gamePlayers[1].getUsername());
        gamePlayers[1].notifyGameStart(gameNumber, 1, gamePlayers[0].getUsername());

        notifyTurn();
	  }
    }

    public void playerGone(hardquorUserSession quitter)
    { hardquorUserSession alone = null;
      if(quitter == gamePlayers[0]) alone = gamePlayers[1];
      else if(quitter == gamePlayers[1]) alone = gamePlayers[0];

      alone.notifyGameQuit(gameNumber, quitter.getUsername());
      ArrayList aloneGames = alone.getUserGames();
      aloneGames.remove(aloneGames.indexOf(this));

	}

    public void notifyTurn()
    { if(gameBoard.winner() == gameBoard.PLAYER_NONE)
      { gamePlayers[gameBoard.turn()].notifyGameTurn(gameNumber);
      } else
      { String winnerName = gamePlayers[gameBoard.winner()].getUsername();
		gamePlayers[0].notifyGameWinner(gameNumber, winnerName);
        gamePlayers[1].notifyGameWinner(gameNumber, winnerName);
	  }
	}

    public void notifyMove(hqMove move)
    { String moveText = "";
      if(move.type() == hqMove.WALL)
      { moveText = moveText + move.a1().x() + "," + move.a1().y() + " ";
        moveText = moveText + move.a2().x() + "," + move.a2().y() + " ";
        moveText = moveText + move.b1().x() + "," + move.b1().y() + " ";
        moveText = moveText + move.b2().x() + "," + move.b2().y();
      } else if(move.type() == hqMove.WALK)
      { moveText = moveText + move.source().x() + "," + move.source().y() + " ";
        moveText = moveText + move.target().x() + "," + move.target().y();
	  }

      gamePlayers[0].notifyGameMove(gameNumber, moveText);
      gamePlayers[1].notifyGameMove(gameNumber, moveText);
	}

    public void tryMove(int[] source, int[] target, hardquorUserSession moveMaker)
    { hqBoardSquare boardSource = gameBoard.getSquare(source[0], source[1]);
      hqBoardSquare boardTarget = gameBoard.getSquare(target[0], target[1]);
      tryMove(new hqMove(boardSource, boardTarget), moveMaker);
	}

	public void tryMove(int[] a1, int[] a2, int[] b1, int[] b2, hardquorUserSession moveMaker)
	{ hqBoardSquare boardA1 = gameBoard.getSquare(a1[0], a1[1]);
      hqBoardSquare boardA2 = gameBoard.getSquare(a2[0], a2[1]);
      hqBoardSquare boardB1 = gameBoard.getSquare(b1[0], b1[1]);
      hqBoardSquare boardB2 = gameBoard.getSquare(b2[0], b2[1]);
      tryMove(new hqMove(boardA1, boardA2, boardB1, boardB2), moveMaker);
	}

	public void tryMove(hqMove move, hardquorUserSession moveMaker)
	{
	  int errorCode = -1;
      int moveMakerNum = -1;
      if(moveMaker == gamePlayers[0]) moveMakerNum = 0;
      else if(moveMaker == gamePlayers[1]) moveMakerNum = 1;

      if(moveMakerNum != gameBoard.turn()) errorCode = gameBoard.ERROR_WRONG_PLAYER;
      else errorCode = gameBoard.isLegalMove(move);

      moveMaker.notifyGameError(gameNumber, errorCode);

      if(errorCode == gameBoard.ERROR_NONE)
      { gameBoard.move(move);
        notifyMove(move);
        notifyTurn();
	  }

	}

	public hardquorUserSession[] getSessions()
	{ return gamePlayers;
    }

	public int getGameNumber()
	{ return gameNumber;
    }

  }

  class hardquorUserSession implements Runnable
  {
    BufferedReader in;
    BufferedWriter out;
    Socket connection;

    String username;
    ArrayList userGames;

    public hardquorUserSession(Socket userConnection)
    { username = null;
      userGames = new ArrayList();

	  try
	  { in = new BufferedReader(new InputStreamReader(userConnection.getInputStream()));
        out = new BufferedWriter(new OutputStreamWriter(userConnection.getOutputStream()));
        connection = userConnection;
	  } catch(IOException e)
	  { log(username,LOG_WARNING,"error tearing i/o streams from socket");
      }

      Thread interaction = new Thread(this);
	  interaction.start();

    }

    public void run()
    { username = null;

      if((username = requestUsername()) != null)
      {
		registerUserSession(username, this);
        notifyUserList();

        String[] command;
        boolean connected = true;

        while(connected == true && (command = readCommand()) != null)
        {
		  if(command.length == 0)
		  {
	      } else if(command[0].equals("SPEAK"))
	      { handleSpeakCommand(command);

	      } else if(command[0].equals("GAMEINVITE"))
	      { handleGameRequestCommand(command);

	      } else if(command[0].equals("GAMEACCEPT"))
	      { handleGameAcceptCommand(command);

	      } else if(command[0].equals("GAMEMOVE"))
	      { handleGameMoveCommand(command);

	      } else if(command[0].equals("GAMEQUIT"))
	      { handleGameQuitCommand(command);

	      } else if(command[0].equals("DISCONNECT"))
	      { connected = false;
		  }
        }
        handleDisconnect();

      // user disconnected before name entered, clean up
	  } else
	  { close();
	  }

    }



    private String requestUsername()
    {
      String reason = "new";
      String[] userCommand = null;

      while(reason != null)
      {
        writeln("SENDUSERNAME" + " " + reason);

        userCommand = readCommand();

        if(userCommand == null)
        { return null;
	    } else if(userCommand.length != 2 || !userCommand[0].equals("USERNAME"))
        { reason = "syntax";
	    } else
	    { reason = isValidUsername(userCommand[1]);
	    }
	  }
	  return userCommand[1];

	}

    public void notifyGameStart(int gameNumber, int playerNumber, String opponent)
    { userGames.add(getGame(gameNumber));
	  writeln("GAMESTART " + gameNumber + " " + playerNumber + " " + opponent);
	}

	public void notifyGameRequest(int gameNumber, int requestMakerPlayerNumber, String requestMaker)
	{ writeln("GAMEINVITE " + gameNumber + " " + requestMakerPlayerNumber + " " + requestMaker);
	}

    public void notifyGameTurn(int gameNumber)
    { writeln("GAMETURN " + gameNumber);
    }

    public void notifyGameWinner(int gameNumber, String gameWinner)
    { writeln("GAMEWINNER " + gameNumber + " " + gameWinner);
    }

    public void notifyGameError(int gameNumber, int errorCode)
    { writeln("GAMEERROR " + gameNumber + " " + errorCode);
    }

    public void notifyGameMove(int gameNumber, String moveText)
    { writeln("GAMEMOVE " + gameNumber + " " + moveText);
    }

    public void notifyGameQuit(int gameNumber, String quitterName)
    { writeln("GAMEQUIT " + gameNumber + " " + quitterName);
    }

	public void notifyUserList()
	{
      String[] usernames = (String[])registeredUsers.keySet().toArray(new String[] { });
      String command = "USERLIST";
      for(int i = 0; i < usernames.length; i++)
      { command = command + " " + usernames[i];
      }
      writeln(command);
	}

    public void notifyUserGone(String username)
    { writeln("USERGONE " + username);
	}

    public void notifyUserNew(String username)
    { writeln("USERNEW " + username);
	}

    public void notifySpeak(int area, String source, String message)
    { writeln("SPEAK " + area + " " + source + " " + message);
    }

    public void handleSpeakCommand(String[] command)
    {
      if(command.length >= 3)
      {
		int area = Integer.valueOf(command[1]).intValue();

		String message = command[2];
		for(int i = 3; i < command.length; i++) message = message + " " + command[i];

        hardquorUserSession[] sessions = getSessions(area);
        for(int i = 0; i < sessions.length; i++)
        { sessions[i].notifySpeak(area, username, message);
        }
	  }
	}

	public void handleGameRequestCommand(String[] command)
	{ if(command.length == 3)
	  { int requesterPlayerNumber = Integer.valueOf(command[1]).intValue();
	    String targetOpponent = command[2];
	    hardquorGame game = new hardquorGame(this, requesterPlayerNumber, targetOpponent);
	  }
	}

	public void handleGameAcceptCommand(String[] command)
	{ if(command.length == 2)
	  { hardquorGame game = getGame(Integer.valueOf(command[1]).intValue());
	    game.requestAccepted();
	  }
	}

	public void handleGameMoveCommand(String[] command)
	{
      hardquorGame game = null;

      if(command.length >= 2) game = getGame(Integer.valueOf(command[1]).intValue());

	  if(game != null && (command.length == 4 || command.length == 6))
	  {


	    if(command.length == 4)
	    {
		  int[] source = breakApartCoordinates(command[2]);
	      int[] target = breakApartCoordinates(command[3]);
	      game.tryMove(source, target, this);

	    } else if(command.length == 6)
	    {
		  int[] a1 = breakApartCoordinates(command[2]);
	      int[] a2 = breakApartCoordinates(command[3]);
		  int[] b1 = breakApartCoordinates(command[4]);
	      int[] b2 = breakApartCoordinates(command[5]);
	      game.tryMove(a1, a2, b1, b2, this);
	    }
	  }
	}

    public void handleDisconnect()
    { handleQuitAllGames();
	  deregisterUserSession(this);
	  close();
    }

    private void close()
    {
	  try
	  { in.close();
	    out.close();
	    connection.close();
	  } catch(IOException e)
	  { log(username,LOG_WARNING,"error closing a connection");
      }
    }

    public void handleGameQuitCommand(String[] command)
    {
      if(command.length == 2)
      { int gameNumber = Integer.valueOf(command[1]).intValue();
        for(int i = 0; i < userGames.size(); i++)
        { hardquorGame game = (hardquorGame)userGames.get(i);
          if(game.getGameNumber() == gameNumber)
          { game.playerGone(this);
            userGames.remove(i);
            activeGames.remove(new Integer(gameNumber));
	      }
        }
	  }
    }

    public void handleQuitAllGames()
    {
      for(int i = 0; i < userGames.size(); i++)
      { hardquorGame game = (hardquorGame)userGames.get(i);
        game.playerGone(this);
        activeGames.remove(new Integer(game.getGameNumber()));
      }
    }

    private int[] breakApartCoordinates(String coordinates)
    { StringTokenizer tok = new StringTokenizer(coordinates, " ,");
      int[] coords = new int[2];
      coords[0] = Integer.valueOf(tok.nextToken()).intValue();
      coords[1] = Integer.valueOf(tok.nextToken()).intValue();
      return coords;
	}

    public String[] breakApartCommand(String command)
    { if(command == null) return null;
  	  ArrayList commandSegments = new ArrayList();
      StringTokenizer tok = new StringTokenizer(command, " \n\t");
      while(tok.hasMoreTokens())
      { commandSegments.add(tok.nextToken());
      }
      return (String[])commandSegments.toArray(new String[] { });
    }

	private void writeln(String line)
	{
      try
      { log(username,LOG_OUT,line);
		out.write(line + "\n");
        out.flush();
      } catch(IOException e)
      { log(username,LOG_OUT,"error writing line " + line);
      }
    }

	private String readln()
	{
      try
      { String line = in.readLine();
        log(username,LOG_IN,line);
        return line;
      } catch(IOException e)
      { log(username,LOG_IN,"Error reading line or raw disconnect.");
      }
      return null;
    }

	private String[] readCommand()
	{ return breakApartCommand(readln());
    }

    public String getUsername()
    { return username;
    }

    public ArrayList getUserGames()
    { return userGames;
    }

  }

  public void log(String facility, int direction, String logText)
  { System.out.println(facility + LOG_DIRECTIONS[direction] + logText);
  }

}